// Copyright (c) 2020 Presto Labs Pte. Ltd.
// Author: linchuan

#include "coin2/exchange/bitmex/api_order/native_private_client.h"

#include <memory>
#include <string>

#include "coin2/exchange/base/api_util/url_params_builder.h"
#include "coin2/exchange/base/order/order.h"
#include "coin2/exchange/base/order/rate_config.h"
#include "coin2/exchange/bitmex/api_util/rate_tracker.h"
#include "coin2/exchange/bitmex/symbology/product.h"

template <typename Container>
std::string ToList(Container container) {
  std::string res;
  for (auto& v : container) {
    if (res.empty()) {
      res += v;
    } else {
      res += ",";
      res += v;
    }
  }
  return res;
}

void BitmexNativePrivateClient::SetMakerOrder(bool is_maker_order) {
  local_rate_tracker_->SetMakerOrder(is_maker_order);
}

auto BitmexNativePrivateClient::GetRestOrderParams(
    const OrderSpec& order_spec,
    int64_t proc_order_id,
    int64_t orig_proc_order_id) -> RestParams {
  RestParams params;

  using coin2::exchange::bitmex_futures::symbology::BitmexFuturesProduct;
  auto product = BitmexFuturesProduct::FromStr(order_spec.product, order_spec.order_created_time);
  auto product_holder = GetProductHolder(product, order_spec.order_created_time);
  auto formatter = product_holder->product_info().order_formatter();
  params.set_symbol(order_spec.product);
  if (order_spec.order_type != OrderType::MARKET_ORDER) {
    params.set_price(formatter.FormatPrice(order_spec.price));
  }
  params.set_order_qty(formatter.FormatQty(order_spec.qty));
  params.set_cl_ord_id(std::to_string(proc_order_id));

  if (orig_proc_order_id > 0) {
    params.set_orig_cl_ord_id(std::to_string(orig_proc_order_id));
  }

  if (order_spec.post_only) {
    params.set_exec_inst("ParticipateDoNotInitiate");
  }
  if (order_spec.order_type == OrderType::MARKET_ORDER) {
    params.set_ord_type("Market");
  } else {
    params.set_ord_type("Limit");
  }
  if (order_spec.duration == coin::proto::OrderDuration::FOK_ORDER) {
    params.set_time_in_force("FillOrKill");
  } else if (order_spec.duration == coin::proto::OrderDuration::IOC_ORDER) {
    params.set_time_in_force("ImmediateOrCancel");
  } else {
    params.set_time_in_force("GoodTillCancel");
  }

  if (order_spec.order_side == OrderSide::BUY_ORDER) {
    params.set_side("Buy");
  } else if (order_spec.order_side == OrderSide::SELL_ORDER) {
    params.set_side("Sell");
  } else {
    NOTREACHED() << "Unknown order side: " << static_cast<int>(order_spec.order_side);
  }

  return params;
}

auto BitmexNativePrivateClient::GetRestCancelParams(const IOrderContext& order_context)
    -> RestParams {
  const auto& order_spec = order_context.order_spec();
  auto params = RestParams();
  params.set_symbol(order_spec.product);
  if (order_context.has_exchange_order_id()) {
    params.set_order_id(order_context.exchange_order_id());
  } else {
    params.set_cl_ord_id(std::to_string(order_context.proc_order_id()));
  }
  return params;
}

BitmexNativePrivateClient::BitmexNativePrivateClient(
    const MarketExchangeApi& mea,
    const NativeClientConfig& config_override) {
  mea_ = mea;
  NativeClientConfig config;
  config.set_rest_host("www.bitmex.com");
  config.set_rest_host_port(443);
  config.MergeFrom(config_override);
  LOG(INFO) << "[BitmexNativePrivateClient] config: " << config.DebugString();

  auths_.Construct<BitmexRestAuth, NativeClientConfig>(config);

  // rate limit tracker
  rate_tracker_ = std::make_unique<BitmexRateTracker>(mea_, config);
  local_rate_tracker_ = static_cast<BitmexRateTracker*>(rate_tracker_.get());
}

std::shared_ptr<AsioHttpContext> BitmexNativePrivateClient::QueryWallet() {
  auto ts = GetCurrentTimestamp();
  if (!HasEnoughRateLimit(ts, ActionType::QUERY)) {
    LOG(ERROR) << "Hit rate limit. QueryWallet failed";
    return {};
  }

  UrlParamsBuilder builder;
  auto* context = GetAuth()->CreateRequestByGetWithSignature("/api/v1/user/wallet", &builder);
  Push(1, ts, ActionType::QUERY);

  context->query_type = OrderSubTopicId::REST_ACCOUNT_BALANCE;
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> BitmexNativePrivateClient::QueryMargin() {
  auto ts = GetCurrentTimestamp();
  if (!HasEnoughRateLimit(ts, ActionType::QUERY)) {
    LOG(ERROR) << "Hit rate limit. QueryMargin failed";
    return {};
  }

  UrlParamsBuilder builder;
  builder.PutGet("currency", "all");
  auto* context = GetAuth()->CreateRequestByGetWithSignature("/api/v1/user/margin", &builder);
  Push(1, ts, ActionType::QUERY);

  context->query_type = OrderSubTopicId::REST_ACCOUNT_BALANCE;
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> BitmexNativePrivateClient::QueryPosition() {
  auto ts = GetCurrentTimestamp();
  if (!HasEnoughRateLimit(ts, ActionType::QUERY)) {
    LOG(ERROR) << "Hit rate limit. QueryPosition failed";
    return {};
  }

  UrlParamsBuilder builder;
  auto* context = GetAuth()->CreateRequestByGetWithSignature("/api/v1/position", &builder);
  Push(1, ts, ActionType::QUERY);

  context->query_type = OrderSubTopicId::REST_ACCOUNT_POSITION;
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> BitmexNativePrivateClient::QueryOrders(const RestParams& params) {
  auto ts = GetCurrentTimestamp();
  if (!HasEnoughRateLimit(ts, ActionType::QUERY)) {
    LOG(ERROR) << "Hit rate limit. QueryOrders failed";
    return {};
  }

  UrlParamsBuilder builder;
  builder.PutGet("symbol", params.symbol());
  builder.PutGet("filter", params.filter());
  builder.PutGet("reverse", params.reverse());
  builder.PutGet("count", params.count());
  auto* context = GetAuth()->CreateRequestByGetWithSignature("/api/v1/order", &builder);
  Push(1, ts, ActionType::QUERY);

  // better checker here?
  if (params.has_filter()) {
    context->query_type = OrderSubTopicId::REST_OPEN_ORDERS;
  } else {
    context->query_type = OrderSubTopicId::REST_ALL_ORDERS;
  }
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> BitmexNativePrivateClient::QueryExecution(
    const RestParams& params) {
  auto ts = GetCurrentTimestamp();
  if (!HasEnoughRateLimit(ts, ActionType::QUERY)) {
    LOG(ERROR) << "Hit rate limit. QueryExecution failed";
    return {};
  }

  UrlParamsBuilder builder;
  builder.PutGet("symbol", params.symbol());
  builder.PutGet("filter", params.filter());
  builder.PutGet("reverse", params.reverse());
  builder.PutGet("count", params.count());
  auto* context = GetAuth()->CreateRequestByGetWithSignature("/api/v1/execution", &builder);
  Push(1, ts, ActionType::QUERY);

  context->query_type = OrderSubTopicId::REST_FILL_ORDERS;
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> BitmexNativePrivateClient::PlaceOrder(
    const RestParams& params,
    const OrderSpec& order_spec) {
  auto ts = GetCurrentTimestamp();
  local_rate_tracker_->SetMakerOrder(order_spec.post_only);
  if (!HasEnoughRateLimit(ts, ActionType::PLACE)) {
    LOG(ERROR) << "Hit rate limit. PlaceOrder failed";
    return {};
  }

  UrlParamsBuilder builder;
  builder.PutPost("symbol", params.symbol());
  builder.PutPost("side", params.side());
  builder.PutPost("price", params.price());
  builder.PutPost("orderQty", params.order_qty());
  builder.PutPost("displayQty", params.display_qty());
  builder.PutPost("stopPx", params.stop_px());
  builder.PutPost("clOrdID", params.cl_ord_id());
  builder.PutPost("ordType", params.ord_type());
  builder.PutPost("timeInForce", params.time_in_force());
  builder.PutPost("execInst", params.exec_inst());
  builder.PutPost("text", params.text());
  auto* context = GetAuth()->CreateRequestByPostWithSignature("/api/v1/order", &builder);
  Push(1, ts, ActionType::PLACE);

  context->query_type = OrderSubTopicId::SUBMIT_RESPONSE;
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> BitmexNativePrivateClient::AmendOrder(const RestParams& params) {
  auto ts = GetCurrentTimestamp();
  if (!HasEnoughRateLimit(ts, ActionType::AMEND)) {
    LOG(ERROR) << "Hit rate limit. AmendOrder failed";
    return {};
  }

  UrlParamsBuilder builder;
  CHECK(params.has_order_id() != params.has_orig_cl_ord_id())
      << "only one of order_id and orig_cl_ord_id can be set!" << params.DebugString();

  if (params.has_order_id() > 0) {
    builder.PutPost("orderID", params.order_id());
  } else {
    builder.PutPost("origClOrdID", params.orig_cl_ord_id());
  }

  builder.PutPost("symbol", params.symbol());
  builder.PutPost("price", params.price());
  builder.PutPost("orderQty", params.order_qty());
  builder.PutPost("leavesQty", params.leaves_qty());
  builder.PutPost("stopPx", params.stop_px());
  builder.PutPost("clOrdID", params.cl_ord_id());
  builder.PutPost("text", params.text());
  auto* context = GetAuth()->CreateRequestByPutWithSignature("/api/v1/order", &builder);
  Push(1, ts, ActionType::AMEND);

  context->query_type = OrderSubTopicId::AMEND_RESPONSE;
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> BitmexNativePrivateClient::CancelSingleOrder(
    const RestParams& params) {
  auto ts = GetCurrentTimestamp();
  if (!HasEnoughRateLimit(ts, ActionType::CANCEL)) {
    LOG(ERROR) << "Hit rate limit. CancelSingleOrder failed";
    return {};
  }

  UrlParamsBuilder builder;
  CHECK(params.has_order_id() != params.has_cl_ord_id())
      << "only one of order_id and cl_ord_id can be set!" << params.DebugString();

  if (params.has_order_id() > 0) {
    builder.PutPost("orderID", params.order_id());
    auto* context = GetAuth()->CreateRequestByDeleteWithSignature("/api/v1/order", &builder);
    Push(1, ts, ActionType::CANCEL);

    context->query_type = OrderSubTopicId::CANCEL_RESPONSE;
    return std::shared_ptr<AsioHttpContext>(context);
  } else {
    builder.PutPost("clOrdID", params.cl_ord_id());
    auto* context = GetAuth()->CreateRequestByDeleteWithSignature("/api/v1/order", &builder);
    Push(1, ts, ActionType::CANCEL);

    context->query_type = OrderSubTopicId::CANCEL_RESPONSE;
    return std::shared_ptr<AsioHttpContext>(context);
  }
}

std::shared_ptr<AsioHttpContext> BitmexNativePrivateClient::CancelOrders(const RestParams& params) {
  auto ts = GetCurrentTimestamp();
  if (!HasEnoughRateLimit(ts, ActionType::CANCEL)) {
    LOG(ERROR) << "Hit rate limit. CancelOrders failed";
    return {};
  }

  UrlParamsBuilder builder;
  builder.PutPost("symbol", params.symbol());
  builder.PutPost("filter", params.filter());
  builder.PutPost("text", params.text());
  auto* context = GetAuth()->CreateRequestByDeleteWithSignature("/api/v1/order/all", &builder);
  Push(1, ts, ActionType::CANCEL);

  context->query_type = OrderSubTopicId::MULTI_CANCEL_RESPONSE;
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> BitmexNativePrivateClient::BulkPlaceOrder(
    const std::vector<RestParams*>& vec_params,
    const OrderSpec& order_spec) {
  auto ts = GetCurrentTimestamp();
  local_rate_tracker_->SetMakerOrder(order_spec.post_only);
  if (!HasEnoughRateLimit(ts, ActionType::PLACE)) {
    LOG(ERROR) << "Hit rate limit. BulkPlaceOrder failed";
    return {};
  }

  UrlParamsBuilder builder;

  std::stringstream ss("[");
  for (auto it = vec_params.begin(); it != vec_params.end(); ++it) {
    JsonWriter single_orders;
    single_orders.PutString("symbol", (*it)->symbol());
    single_orders.PutString("side", (*it)->side());
    single_orders.PutString("price", (*it)->price());
    single_orders.PutString("orderQty", (*it)->order_qty());
    single_orders.PutString("displayQty", (*it)->display_qty());
    single_orders.PutString("stopPx", (*it)->stop_px());
    single_orders.PutString("clOrdID", (*it)->cl_ord_id());
    single_orders.PutString("ordType", (*it)->ord_type());
    single_orders.PutString("timeInForce", (*it)->time_in_force());
    single_orders.PutString("execInst", (*it)->exec_inst());
    single_orders.PutString("text", (*it)->text());
    ss << single_orders.ToJsonString();
    if (std::next(it) != vec_params.end()) {
      ss << ",";
    }
  }
  ss << "]";

  builder.PutPost("orders", ss.str());
  auto* context = GetAuth()->CreateRequestByPostWithSignature("/api/v1/order/bulk", &builder);
  Push(1, ts, ActionType::PLACE);
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> BitmexNativePrivateClient::BulkAmendOrder(
    const std::vector<RestParams*>& vec_params) {
  auto ts = GetCurrentTimestamp();
  if (!HasEnoughRateLimit(ts, ActionType::AMEND)) {
    LOG(ERROR) << "Hit rate limit. BulkAmendOrder failed";
    return {};
  }

  UrlParamsBuilder builder;

  std::stringstream ss("[");
  for (auto it = vec_params.begin(); it != vec_params.end(); ++it) {
    JsonWriter single_orders;
    if ((*it)->has_order_id() > 0) {
      single_orders.PutString("orderID", (*it)->order_id());
    } else {
      single_orders.PutString("origClOrdID", (*it)->orig_cl_ord_id());
    }
    single_orders.PutString("symbol", (*it)->symbol());
    single_orders.PutString("price", (*it)->price());
    single_orders.PutString("orderQty", (*it)->order_qty());
    single_orders.PutString("leavesQty", (*it)->leaves_qty());
    single_orders.PutString("stopPx", (*it)->stop_px());
    single_orders.PutString("clOrdID", (*it)->cl_ord_id());
    single_orders.PutString("text", (*it)->text());
    ss << single_orders.ToJsonString();
    if (std::next(it) != vec_params.end()) {
      ss << ",";
    }
  }
  ss << "]";

  builder.PutPost("orders", ss.str());
  auto* context = GetAuth()->CreateRequestByPutWithSignature("/api/v1/order/bulk", &builder);
  Push(1, ts, ActionType::AMEND);
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> BitmexNativePrivateClient::PlaceOrderNorm(
    const OrderExecution& order_exe) {
  const auto& order_spec = *order_exe.order_spec;
  const auto proc_order_id = order_exe.proc_order_id;
  // TODO(xguo) fix
  auto params = GetRestOrderParams(order_spec, proc_order_id, 0);
  return PlaceOrder(params, order_spec);
}

std::shared_ptr<AsioHttpContext> BitmexNativePrivateClient::CancelOrderNorm(
    const OrderExecution& order_exe) {
  auto params = GetRestCancelParams(*order_exe.oc);
  return CancelSingleOrder(params);
}

std::shared_ptr<AsioHttpContext> BitmexNativePrivateClient::SubmitOrderExecution(
    const OrderExecutionProto& order) {
  switch (order.type()) {
    case OrderExecutionProto::SUBMIT_ORDER: {
      auto order_spec = OrderSpec::FromProto(order.order_spec());
      auto params =
          GetRestOrderParams(*order_spec, order.proc_order_id(), order.orig_proc_order_id());
      return PlaceOrder(params, *order_spec);
    }
    case OrderExecutionProto::CANCEL_ORDER: {
      auto order_spec = OrderSpec::FromProto(order.order_spec());
      auto params =
          GetRestOrderParams(*order_spec, order.proc_order_id(), order.orig_proc_order_id());
      return CancelOrders(params);
    }
    default: {
      return nullptr;
    }
  }
}

std::shared_ptr<AsioHttpContext> BitmexNativePrivateClient::SubmitRestQuery(
    const RestQueryProto& query) {
  RestParams params;
  switch (query.type()) {
    case RestQueryProto::QUERY_ACCOUNT_BALANCE: {
      return QueryWallet();
    }
    case RestQueryProto::QUERY_ACCOUNT_POSITION: {
      return QueryPosition();
    }
    case RestQueryProto::QUERY_OPEN_ORDERS: {
      params.set_filter("{\"open\": true}");
      return QueryOrders(params);
    }
    default: {
      return nullptr;
    }
  }
}
